namespace cxxcall
{

#define SET_REG(Name, Value)   \
    __asm__ __volatile__ ("movq %0, %" Name : : "g" (Value))

#define SET_RAX(Value)    SET_REG ("%rax", ((intptr_t)(Value)))
#define SET_RDX(Value)    SET_REG ("%rdx", ((intptr_t)(Value)))
#define SET_XMM0(Value)   SET_REG ("%xmm0", Value)

#ifndef _WIN32

static void
cb_receiver (uintptr_t w1, uintptr_t w2, uintptr_t w3, uintptr_t w4,
  uintptr_t w5, uintptr_t w6, double f1, double f2, double f3,
  double f4, double f5, double f6, double f7, double f8)
{
#ifndef __clang__
  register cb_fct *env __asm__ ("r10");
#else
  /* clang can't be convinced to generate the call below, so we have to
   * jump through some hoops. This is potentially more portable, but it
   * generates worse code, so it isn't used in the general case. */
  cb_fct *env;
  __asm__ ("movq %%r10, %0" : "+r" (env));
#endif
  cb_data data;

#define I_(x)   data.iargs[x - 1] = w##x
#define F_(x)   data.fargs[x - 1] = f##x

  I_(1);
  I_(2);
  I_(3);
  I_(4);
  I_(5);
  I_(6);
  
  F_(1);
  F_(2);
  F_(3);
  F_(4);
  F_(5);
  F_(6);
  F_(7);
  F_(8);

#undef I_
#undef F_

  data.flags = 0;
  data.curr = (uintptr_t)__builtin_frame_address (0) + 2 * WSIZE;
  data.rtype = type::void_t;
  env->fn (&data, env->arg);

  switch (data.rtype)
    {
      case type::void_t:
        break;

#define IRET(Typecode, Member)   \
  case type::Typecode:   \
    SET_RAX (data.tmp.Member);   \
    break

      IRET (schar_t, sch);
      IRET (uchar_t, uch);
      IRET (sshort_t, shrt);
      IRET (ushort_t, ushrt);
      IRET (sint_t, ival);
      IRET (uint_t, uival);
      IRET (slong_t, lval);
      IRET (ulong_t, ulval);
      IRET (sllong_t, llval);
      IRET (ullong_t, ullval);

      case type::float_t:
        SET_XMM0 (data.tmp.flt);
        break;

      case type::double_t:
        SET_XMM0 (data.tmp.dbl);
        break;

      case type::ldouble_t:
        __asm__ __volatile__ ("fldt %0" : : "m" (data.tmp.ldbl));
        break;

      case type::struct_t:
        if (!(data.flags & av_base::reg_struct_ret))
          break;

        {
          size_t size = data.rsize;
          auto wp = (uintptr_t *)((uintptr_t)data.raddr & ~(WSIZE - 1));
          auto start = (uintptr_t)data.raddr & (WSIZE - 1), end = start + size;

          if (size <= WSIZE)
            { // Result fits in %rax
              if (end <= WSIZE)
                SET_RAX ((*wp & ((2ul << (end * 8 - 1)) - 1)) >> (start * 8));
              else
                SET_RAX (((*wp >> (start * 8)) | (wp[1] & ((2ul <<
                  (end * 8 - WBITS - 1)) - 1) << (WBITS - start * 8))));
            }
          else if (end <= 2 * WSIZE)
            {
              auto m1 = (2ul << (end * 8 - WBITS - 1)) - 1;
              auto rax = (*wp >> (start * 8)) | ((wp[1] & m1) <<
                (WSIZE * 4 - start * 4) << (WSIZE * 4 - start * 4));
              auto rdx = (wp[1] & m1) >> (start * 8);

              SET_RAX (rax);
              SET_RDX (rdx);
            }
          else
            {
              auto rax = (*wp >> (start * 8)) | (wp[1] << (WBITS - start * 8));
              auto rdx = ((wp[1] >> (start * 8)) | ((wp[2] &
                ((2ul << (end * 8 - 2 * WBITS - 1)) - 1)) <<
                (WBITS - start * 8)));

              SET_RAX (rax);
              SET_RDX (rdx);
            }
        }
    }

#undef IRET
#undef I_
#undef F_

}

#else

static void
cb_receiver (uintptr_t w1, uintptr_t w2, uintptr_t w3, uintptr_t w4,
             uintptr_t first, double f1, double f2, double f3, double f4)
{
#ifndef __clang__
  register cb_fct *env __asm__ ("r10");
#else
  cb_fct *env;
  __asm__ ("movq %%r10, %0" : "+r" (env));
#endif
  cb_data data;

  // Save the integer registers in the stack.
  (&first)[-4] = w1;
  (&first)[-3] = w2;
  (&first)[-2] = w3;
  (&first)[-1] = w4;
  data.fargs[0] = f1;
  data.fargs[1] = f2;
  data.fargs[2] = f3;
  data.fargs[3] = f4;
  data.flags = 0;
  data.rtype = type::void_t;
  data.raddr = nullptr;
  data.anum = 0;
  data.curr = (uintptr_t)(&first - 4);
  env->fn (data, env->arg);

  switch (data.rtype)
    {
      case type::void_t:
        break;

#define IRET(Typecode, Member)   \
  case type::Typecode:   \
    SET_RAX (data.tmp.Member);   \
    break

      IRET (schar_t, sch);
      IRET (uchar_t, uch);
      IRET (sshort_t, shrt);
      IRET (ushort_t, ushrt);
      IRET (sint_t, ival);
      IRET (uint_t, uival);
      IRET (slong_t, lval);
      IRET (ulong_t, ulval);
      IRET (sllong_t, llval);
      IRET (ullong_t, ullval);

      case type::float_t:
        SET_XMM0 (data.tmp.flt);
        break;

      case type::double_t:
        SET_XMM0 (data.tmp.dbl);
        break;

      case type::ldouble_t:
        __asm__ __volatile__ ("fldt %0" : : "m" (data.tmp.ldbl));
        break;

      case type::struct_t:
        if (!(data.flags & av_base::reg_struct_ret))
          iret = (uintptr_t)data.raddr;
        else if (data.rsize == 1 || data.rsize == 2 ||
            data.rsize == 4 || data.rsize == 8)
          {
            size_t size = data.rsize;
            auto wp = (uintptr_t *)((uintptr_t)data.raddr & ~(WSIZE - 1));
            auto start = (uintptr_t)data.raddr & (WSIZE - 1);
            auto end = start + size;

            if (end <= WSIZE)
              SET_RAX ((wp[0] & ((2ul << (end * 8 - 1)) - 1)) >> (start * 8));
            else
              SET_RAX ((wp[0] >> (start * 8)) | ((wp[1] &
                ((2ul << (end * 8 - WBITS - 1)) - 1)) << (WBITS - start * 8)));
          }

    }
}

#undef IRET

#endif

#undef SET_RAX
#undef SET_RDX

void cb_data::fill (void *outp, void (*fn) (cb_data *, void *), void *argx)
{
  void *fct = (void *)cb_receiver;

#ifndef CXXCALL_WIDE
  // x32 ABI.
  void **arg = (void **)((char *)outp + 16);

  auto p = (uintptr_t *)outp;
  *p++ = 0xba41 | ((uintptr_t)arg << 16);
  *p++ = 0xb80000 | ((uintptr_t)fct << 24) | ((uintptr_t)arg >> 16);
  *p++ = 0xff000000 | ((uintptr_t)fct >> 8);
  *p++ = 0xe0;

#else
  void **arg = (void **)((char *)outp + 24);

  uint16_t tmp[] = { 0xba49, (uint16_t)((uintptr_t)arg & 0xffff) };
  memcpy (outp, tmp, sizeof (tmp));
  outp = (char *)outp + sizeof (tmp);

  *(uint32_t *)outp = (uint32_t)(((uintptr_t)arg >> 16) & 0xffffffff);
  outp = (char *)outp + sizeof (uint32_t);

  tmp[0] = (uint16_t)(((uintptr_t)arg >> 48) & 0xffff);
  tmp[1] = 0xb848;
  memcpy (outp, tmp, sizeof (tmp));
  outp = (char *)outp + sizeof (tmp);

  *(uint32_t *)outp = (uint32_t)((uintptr_t)fct & 0xffffffff);
  outp = (char *)outp + sizeof (uint32_t);
  *(uint32_t *)outp = (uint32_t)(((uintptr_t)fct >> 32) & 0xffffffff);
  outp = (char *)outp + sizeof (uint32_t);
  *(uint16_t *)outp = 0xe0ff;
#endif

  arg[0] = (void *)fn, arg[1] = argx;
}

#ifndef CXXCALL_WIDE
static const size_t TRAMP_LEN = 13;
#else
static const size_t TRAMP_LEN = 22;
#endif

} // namespace cxxcall
